[免杀学习]杀毒软件扫描浅析(下)
下篇较上篇所讲述的内存属性,内容的扫描来说相对简单,也比较好理解
文件内容扫描
首先创建一个txt文档,随便写点内容,比如1234567890
源代码
1 |
|
逐行讲解
1 | OVERLAPPED __Overlapped = { 0 }; |
OVERLAPPED结构体
这个结构的用处是在异步(或重叠)输入输出(I/O)操作中存储一些信息,比如请求的状态、传输的字节数、文件的位置和事件的句柄。这样可以让你在不阻塞程序的情况下,对文件或其他设备进行读写操作,并在操作完成时得到通知。
参数 | 类型 | 说明 |
---|---|---|
Internal | ULONG_PTR | I/O请求的状态码 |
InternalHigh | ULONG_PTR | I/O请求传输的字节数 |
Offset | DWORD | 开始I/O请求的文件位置的低位部分 |
OffsetHigh | DWORD | 开始I/O请求的文件位置的高位部分 |
Pointer | PVOID | 保留给系统使用,初始化为零后不要使用 |
hEvent | HANDLE | 一个事件的句柄,当操作完成时,系统会把它设置为有信号状态 |
Internal和InternalHigh是用来存储I/O请求的状态和传输的字节数的。当你发起一个异步(或重叠)I/O请求时,系统会把Internal成员设置为STATUS_PENDING,表示操作还没有开始。当请求完成时,系统会把Internal成员设置为完成请求的状态码。你可以通过调用GetOverlappedResult函数来获取这个状态码。系统也会把InternalHigh成员设置为I/O请求传输的字节数。
Offset和OffsetHigh就是读取文件内容的位置偏移量,比如设置Offset为8,则就从文件内容的第8+1个位置开始读
1 |
|
其实在windows api中,偏移量和Pointer是在同一片union中定义的
1 | union { |
union是一种特殊的数据类型,它可以存储不同类型的数据,但是只能同时存储其中一个成员的值。union中的成员共享同一块内存空间,它们的大小等于最大成员的大小。union可以用来节省内存空间,或者实现不同类型之间的转换。
在OVERLAPPED结构中,union是用来表示文件位置的不同方式。文件位置是一个64位整数,由Offset和OffsetHigh两个32位整数组合而成。但是有些情况下,你可能不需要指定一个具体的文件位置,而是使用一个指针来表示一个内存地址或一个回调函数。这时候,你可以使用Pointer成员来代替Offset和OffsetHigh成员。
这样设计的原因可能是为了兼容不同的I/O操作和设备,或者为了保留一些未来的扩展性。你可以根据你的需要选择使用union中的哪个成员。
1 | HANDLE hFile = CreateFile(IP_path,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); |
CreateFile函数是用来创建或打开一个文件或I/O设备的。你必须指定文件或设备的名称,创建方式,以及其他属性。
函数返回一个句柄,你可以用这个句柄来对文件进行操作
1 | HANDLE CreateFileA( |
补充
dwDesiredAccess的预定义常量如下:
1 | // |
对dwShareMode的”共享模式”的解释:
dwShareMode参数是用来指定对文件或设备的共享模式的,也就是说,你是否允许其他进程同时打开同一个文件或设备,并进行读、写、删除等操作。你可以使用下表中的值来指定不同的共享模式:
值 | 含义 |
---|---|
0 | 不允许其他进程共享文件或设备。如果文件已经被其他进程打开,CreateFile会失败。这也称为独占访问。 |
FILE_SHARE_READ | 允许其他进程打开文件或设备进行读取操作。如果没有指定这个值,任何请求读取的进程都会被拒绝,即使调用进程也没有请求读取访问。 |
FILE_SHARE_WRITE | 允许其他进程打开文件或设备进行写入操作。如果没有指定这个值,任何请求写入的进程都会被拒绝,即使调用进程也没有请求写入访问。 |
FILE_SHARE_DELETE | 允许其他进程打开文件或设备进行删除操作。如果没有指定这个值,任何请求删除的进程都会被拒绝,即使调用进程也没有请求删除访问。 |
lpSecurityAttributes:
SECURITY_ATTRIBUTES结构体是用来包含一个对象的安全描述符,并指定通过这个结构体获取的句柄是否可以被继承的
1 | typedef struct _SECURITY_ATTRIBUTES { |
其中,各个成员的含义是:
- nLength:这个结构体的大小,以字节为单位。设置这个值为SECURITY_ATTRIBUTES结构体的大小。
- lpSecurityDescriptor:一个指向SECURITY_DESCRIPTOR结构体的指针,它控制了对对象的访问。如果这个成员的值为NULL,对象会被分配调用进程的访问令牌关联的默认安全描述符。
- bInheritHandle:一个布尔值,指定当创建一个新进程时,返回的句柄是否可以被继承。如果这个成员为TRUE,新进程继承句柄。
dwFlagsAndAttributes:
标志和属性可以分为两类:文件标志和文件属性。文件标志控制或影响系统如何缓存与句柄相关的数据,比如是否使用缓冲区、是否优化随机或顺序访问等。文件属性指定了文件的一些元数据,比如是否只读、隐藏、系统、压缩等。
hTemplateFile和函数返回的句柄的区别:
hTemplateFile参数是一个有效的文件句柄,它包含了一些预定义的属性(比如安全描述符)来创建新文件。如果为NULL,这些属性会从父目录继承。这个参数只有在创建新文件时才有效。
CreateFile函数返回的句柄是一个用来访问文件或设备的标识符,它可以用于不同类型的I/O操作,取决于文件或设备和指定的标志和属性。这个句柄在关闭之前一直有效,可以被继承或不被继承。
hTemplateFile参数和CreateFile函数返回的句柄的区别是:
- hTemplateFile参数是一个输入参数,用来提供新文件的一些属性。CreateFile函数返回的句柄是一个输出参数,用来标识打开或创建的文件或设备。
- hTemplateFile参数只有在创建新文件时才有效,而CreateFile函数返回的句柄可以用于打开或创建文件或设备。
- hTemplateFile参数只包含一些预定义的属性,而CreateFile函数返回的句柄可以用于读取或写入文件或设备的数据和元数据。
1 | int filesucc = ReadFileEx(hFile, lpBuffer, sizeof(lpBuffer), &__Overlapped, NULL); |
ReadFileEx函数定义:
1 | ReadFileEx( |
对于 lpCompletionRoutine的通俗解释:
lpCompletionRoutine参数是一个指向一个函数的指针,这个函数是你自己写的,它的作用是在ReadFileEx函数读取文件或设备的数据完成后,告诉你读取操作的结果,比如成功还是失败,读取了多少字节,以及一些其他信息。这个函数只有在你的程序在等待其他事情的时候才会被调用,这样就不会浪费时间。你可以在这个函数里面做一些后续的处理,比如显示数据,保存数据,或者继续读取其他数据。但是你不能在这个函数里面做一些可能会影响程序运行的事情,比如退出程序,或者再次调用ReadFileEx函数。
举个例子,
当你调用ReadFileEx函数。这个函数会把你的读取请求发送给系统,并且立即返回。这样你的程序就不会被阻塞,而是可以继续执行其他代码。但是系统并没有忘记你的请求,它会在后台处理你的读取操作,并且在完成后通知你。
为了接收系统的通知,你需要让你的程序进入一个可以接收异步通知的状态,这个状态就叫做可警告等待状态(alertable wait state)。有一些函数可以让你的程序进入这个状态,比如SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, 或 WaitForSingleObjectEx。这些函数都有一个参数叫做bAlertable,如果你把它设置为TRUE,就表示你想让你的程序进入可警告等待状态。如果你把它设置为FALSE,就表示你不想让你的程序进入可警告等待状态。
当你的程序进入可警告等待状态时,它就可以接收到系统发送的异步通知,并且调用你指定的lpCompletionRoutine函数来处理结果。这个函数会接收到读取操作的结果,比如成功还是失败,读取了多少字节,以及一些其他信息。然后你可以在这个函数里面做一些后续的处理,比如显示数据,保存数据,或者继续读取其他数据。但是你不能在这个函数里面做一些可能会影响程序运行的事情,比如退出程序,或者再次调用ReadFileEx函数。
当你的程序不再需要等待其他事情时,它就会退出可警告等待状态,并且不会再接收到任何通知。这样你就完成了一个异步读取操作,并且利用了系统提供的通知机制。
剩下的就是打印读取到缓冲区的内容了,然后关闭文件句柄
注册表监控
前置知识
Windows 注册表是一个分层数据库,其中包含对 Windows 操作系统和运行在 Windows 上的应用程序和服务的配置数据。注册表的数据以树状结构组织,树中的每个节点称为键,每个键可以包含值和数据。
每个键(节点)可以有多个值和数据
值和数据可以存储各种类型的信息,例如字符串、数字、二进制数据等。
注册表有五个主要的键,分别是:
- HKEY_CLASSES_ROOT:一个注册表的根键,它主要是用来记录不同类型的文件和程序之间的关系。比如说,你有一个 .txt 的文本文件,你想用记事本打开它,那么就需要在 HKEY_CLASSES_ROOT 里面找到 .txt 的扩展名,然后看它对应的是什么程序,然后就可以用那个程序打开它。
- HKEY_CURRENT_USER:包含当前登录用户的配置信息,例如桌面设置、历史记录、收藏夹等。
- HKEY_LOCAL_MACHINE:包含本地计算机的配置信息,例如硬件设备、系统服务、软件安装等。
- HKEY_USERS:包含所有用户的配置信息,每个用户都有一个子键,其名称是用户的安全标识符(SID)。
- HKEY_CURRENT_CONFIG:包含当前硬件配置的信息,例如显示器分辨率、字体、颜色等。
源代码
1 |
|
逐行讲解
1 | HANDLE hNotify = CreateEvent(NULL, FALSE, TRUE, "RegeditNotifyChanged"); |
这个函数是用来创建或打开一个命名或无名的事件对象,并返回一个对象的句柄。
事件对象是一种同步对象,可以用来通知一个或多个等待的线程发生了某个事件
CreateEvent函数定义
1 | CreateEventA( |
补充
bManualReset:
bManualReset 这个参数是用来决定事件对象的类型的。事件对象有两种类型:手动重置事件对象和自动重置事件对象。
手动重置事件对象是这样的:当你创建它时,你可以指定它的初始状态是信号状态还是非信号状态。然后,你可以用 SetEvent 函数来将它的状态改变为信号状态,或者用 ResetEvent 函数来将它的状态改变为非信号状态。 这些操作都需要你手动去做,系统不会自动帮你做。
自动重置事件对象是这样的:当你创建它时,你也可以指定它的初始状态是信号状态还是非信号状态。然后,你可以用 SetEvent 函数来将它的状态改变为信号状态,但是你不能用 ResetEvent 函数来将它的状态改变为非信号状态。 因为系统会在每次有一个等待线程被释放后,自动将事件对象的状态重置为非信号状态。 这就是为什么叫做自动重置事件对象。
所以,bManualReset 这个参数就是用来控制你想要创建哪种类型的事件对象的。如果你想要创建一个手动重置事件对象,那么你就把这个参数设为 TRUE;如果你想要创建一个自动重置事件对象,那么你就把这个参数设为 FALSE。
lpName:
虽然我们在后续的代码中只需要用到事件句柄,但这个事件名的作用是可以在不同的进程中共享这个事件对象,或者打开一个已经存在的命名事件对象。
1 | if (RegOpenKeyEx(hKey_, path_, 0, KEY_NOTIFY, &hRegKey) != ERROR_SUCCESS) |
1 | //函数定义 |
RegOpenKeyExA用于打开注册表的键
hKey:
一个已经打开的注册表键的句柄,可以是由RegCreateKeyEx或RegOpenKeyEx函数返回的句柄,也可以是一些预定义的键,如HKEY_CLASSES_ROOT, HKEY_CURRENT_USER等。
lpSubKey:
要打开的子键的名称,不区分大小写。如果这个参数为NULL或空字符串,那么函数会将phkResult指向的键设置为hKey相同
ulOptions:
指定打开键时要应用的选项,目前只有一个可用的选项,就是REG_OPTION_OPEN_LINK,表示要打开的键是一个符号链接。符号链接是一种特殊的键,它指向另一个键,可以用于跨不同根键或不同机器访问注册表信息。
samDesired:
指定要打开键时所需的访问权限掩码,这个掩码决定了对该键可以进行哪些操作,如查询、设置、枚举、删除等。如果该键的安全描述符不允许调用进程拥有所需的访问权限,那么函数会失败,并返回ERROR_ACCESS_DENIED错误码。
phkResult:
一个指向句柄的指针,用于接收打开的子键的句柄。如果打开的子键不是预定义的键,那么在使用完毕后应该调用RegCloseKey函数来关闭该句柄。
1 | if (RegNotifyChangeKeyValue(hRegKey, TRUE, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES | REG_NOTIFY_CHANGE_LAST_SET, hNotify, TRUE) != ERROR_SUCCESS) |
RegNotifyChangeKeyValue是一个用于监控注册表键变化的函数。
RegNotifyChangeKeyValue函数定义:
1 | RegNotifyChangeKeyValue( |
补充:
dwNotifyFilter的可选值:
REG_NOTIFY_CHANGE_NAME:如果添加或删除了子键,就通知调用者。
REG_NOTIFY_CHANGE_ATTRIBUTES:如果改变了键的属性,如安全描述符信息,就通知调用者。
REG_NOTIFY_CHANGE_LAST_SET:如果改变了键的值,如添加、删除或修改了值,就通知调用者。
REG_NOTIFY_CHANGE_SECURITY:如果改变了键的安全描述符,就通知调用者。
REG_NOTIFY_THREAD_AGNOSTIC:表示注册的生命周期不应该与发起RegNotifyChangeKeyValue调用的线程的生命周期绑定。(只支持Windows 8及以上版本)这样做的好处就是即使当前线程结束了,监控函数依然在起作用,直到主动取消它或者程序退出
1 | if (WaitForSingleObject(hNotify, INFINITE) != WAIT_FAILED) |
WaitForSingleObject函数是一个用于等待某个对象变成信号状态的函数。
信号状态是一种表示对象是否可用或者满足某种条件的状态。不同类型的对象有不同的信号状态的含义,比如:
- 事件对象:信号状态表示事件已经发生,非信号状态表示事件还没有发生。
- 互斥对象:信号状态表示互斥没有被占用,非信号状态表示互斥已经被占用。
- 信号量对象:信号状态表示信号量的计数大于零,非信号状态表示信号量的计数等于零。
- 定时器对象:信号状态表示定时器已经到期,非信号状态表示定时器还没有到期。
WaitForSingleObject的作用是检查指定的对象是否已经是信号状态。
如果是,那么函数就会立即返回,并告诉您对象已经是信号状态了。
如果不是,那么函数就会让程序等待一段时间,直到对象变成信号状态或者超时了为止。
函数定义:
1 | WaitForSingleObject( |
补充:
dwMilliseconds:
如果指定了一个非零的值,那么函数就会在对象变成信号状态或者等待时间到了之后返回。
如果指定了零,那么函数就不会等待,而是直接检查对象是否已经是信号状态,并立即返回。
如果指定了INFINITE,那么函数就会无限期地等待,直到对象变成信号状态为止。
WaitForSingleObject函数有一个返回值,它是一个表示函数执行结果的值。它可以是以下几种值之一:
- WAIT_OBJECT_0:表示对象已经变成了信号状态。
- WAIT_ABANDONED:表示对象是一个互斥对象,并且它之前被占用的线程在退出时没有释放它。这种情况下,系统会把互斥的所有权转给调用者,并把互斥设置为非信号状态。但是,由于之前占用互斥的线程可能没有正确地完成它要保护的数据操作,所以需要检查数据是否一致。
- WAIT_TIMEOUT:表示等待时间已经到了,但是对象还没有变成信号状态。
- WAIT_FAILED:表示函数执行失败了。可以调用GetLastError函数来获取具体的错误信息。
剩下就是主函数里的提供要监控的根键和子键,以及函数内部的关闭句柄的操作,在这里不再赘述
总结:
主函数->定义要监控的根键,子键->RegeditNotifyChanged监控函数->创建一个事件对象用于监控注册表的变化->打开注册表(获取注册表句柄)->调用RegNotifyChangeKeyValue函数进行监控,结果将通过事件对象的状态来体现->WaitForSingleObject函数监控事件对象的信号状态(阻塞当前线程)->修改注册表对于键->hNotify事件对象的状态变为信号状态->WaitForSingleObject获取到返回值->打印信息
来自Microsoft官方的提示:
此函数不能用于检测使用 RegRestoreKey函数生成的注册表的更改。